feat(chart): chart-view redesign — config bar, autoChart, 5 types via Chart.js#20
Merged
Conversation
…s via Chart.js Replace the single hard-wired bar chart with a real chart surface matching the Claude Design handoff: a config bar (Type / X / Y / multi-series toggle / group-by Series), an autoChart heuristic that classifies columns from their ClickHouse types (temporal → line, categorical → horizontal bar, ordinal → column), and five renderers (h-bar / column / line / area / pie) with multi-series and group-by pivot, themed to the CSS tokens. Rendering now uses Chart.js — the one bundled runtime dependency, inlined into dist/sql.html so the page still makes zero third-party requests. This relaxes CLAUDE.md hard rule #4 (recorded there, in the README, and build.mjs). To keep the coverage model intact, all role/axis/pivot/scale math and the Chart.js config builder are pure in src/core/chart-data.js (100%), and the `new Chart()` call is an injected seam (app.Chart) so the DOM wrapper stays fully tested. Chart config persists per query tab (tab.chartCfg/chartKey), re-derived on schema change; live Chart instances are destroyed on re-render to avoid leaks. npm test: 488 passed, per-file gate green. Build: 313 KB self-contained artifact. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Wtzg34E1iGzKm11TArRpko
201218a to
2c2dc45
Compare
…t, tighten "All measures" - Show "first 500 of N rows" in the config bar when a result exceeds the chart's CHART_ROW_CAP, so the truncation is never silent (the table still shows all). - chartRole reuses format.js:isNumericType on the stripped type instead of a second copy of the numeric regex. - "All measures" now targets only true measures and excludes the current X column, so it can't plot an ordinal axis (e.g. month) against itself. - Comment chartNumFmt's deliberate divergence from formatRows. (Reviewer's sort-on-every-config-click note left as-is: sortRows no-ops without an active sort, and caching only the chart's sort would diverge from the table.) npm test: 490 passed, per-file gate green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Wtzg34E1iGzKm11TArRpko
`.res-body` is display:block, so `.chart-view { flex: 1 }` was inert: the view
collapsed to the config bar's height and Chart.js sized the canvas to a few
pixels — a config bar with no visible chart. Use `height: 100%` (the block
parent has a definite height) for `.chart-view` and `.chart-empty` so the
canvas wrapper gets real height.
Not caught earlier: happy-dom does no layout, and the manual browser harness
attached the canvas into a pre-sized container, bypassing the real `.res-body`
parent. Verified live against an otel system.query_log result.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wtzg34E1iGzKm11TArRpko
Vitest 2.x defaults to `pool: 'forks'`, which fans out to (cpus-1) child node processes via tinypool — 11 on a 12-core box. On normal exit those should be reaped, but a detached swarm can survive a run and accumulate across runs until it saturates CPU/RAM. Switch to `pool: 'threads'` (capped at 4): worker threads live inside the single vitest process, so they die with the parent and can never become orphaned OS processes. Safe here — the suite is pure ES modules + happy-dom with no native deps. Also faster (happy-dom env setup 21.7s -> 3.0s, total 6.1s -> 2.3s) since threads skip per-fork environment spin-up. Belt-and-suspenders: a `pretest` hook kills any pre-existing stray vitest processes before a run. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016SGQKuUW7a12hBSwmwSkYa
…bugs
Persistence
- Save/restore the per-tab chart config (type/X/Y/multi/series) with saved
queries and share links. Saved entries gain an optional { cfg, key }; share
links carry a tagged JSON envelope (back-compatible with legacy SQL-only
hashes); export/import round-trip it. Configs are deep-cloned on save/restore
so a restored chart can't mutate its source, and a chartCfgValid guard rejects
out-of-range indices from a hand-edited link/import.
Chart-view fixes
- chartLabel keeps day granularity (YYYY-MM-DD, display-only); buildChartData
SUM-aggregates per cell instead of last-wins, so duplicate buckets combine.
- ORDINAL_RE anchored (^bucketword s?$) so measures like monthly_revenue are no
longer misread as ordinal axes.
- running guard runs before chartCfgFor, so a still-settling result can't stamp
over a restored config.
- normalizeChartCfg clears a Series equal to X and folds pie to single-measure /
no-series; chartCfgFor normalizes on every restore.
- renderChart plots rows in query order, independent of the table sort (also
drops the wasteful full-result sort before the 500-row cap).
- chartColors resolves --mono to a real font stack for canvas ticks/legend.
- .chart-view scrolls and the canvas keeps a min height instead of crushing to 0.
- pretest pkill scoped to this project's vitest config.
523 tests pass; pure/render layers at 100/100/100/100. Verified e2e on antalya
against ontime.fact_ontime.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016SGQKuUW7a12hBSwmwSkYa
…ay labels, scope pretest - chartCfgFor is now the single place that folds chart-config invariants; the Type/X/Series handlers just mutate + re-render (drop their redundant normalizeChartCfg calls). - chartLabel keeps date+HH:MM for an intraday timestamp (date-only for a Date or midnight DateTime), so two same-day buckets no longer collapse to the same tick text. Grouping is still on the raw value. Removed the now-unused ISO_DATE_RE. - pretest pkill pattern requires both `vitest` and `--config tests/vitest.config.ts` so it can't match an unrelated process that merely has the path in its argv. 525 tests pass; chart-data.js at 100/100/100/100. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016SGQKuUW7a12hBSwmwSkYa
- Saved queries now persist the result view (Table/JSON/Chart) alongside the
chart config; saveQuery captures state.resultView (the transient raw view is
not saved), and saved-io export/import/merge carry it.
- Opening a saved query reopens that view and runs immediately: the saved-row
click calls run({ view }), and run() now starts in the given view (or Table
when absent/unknown) instead of always resetting to Table. History rows also
re-run on click (Table view).
- View ownership lives in run() (which already resets resultView); loadIntoNewTab
stays responsible only for tab-level state (sql/savedId/chart).
528 tests pass; core/state/render layers at their per-file gates. Verified e2e
on antalya: a one-click saved-Chart query auto-runs straight into its chart.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016SGQKuUW7a12hBSwmwSkYa
…143) `pkill -f` matches full command lines, so the hook's own shell — whose argv contains the pattern string — was a match, and pkill SIGTERM'd its parent (`npm test`) before `|| true` could run. CI fails at the pretest step with exit 143 before vitest starts; it only passed locally because I'd been running the vitest binary directly, bypassing the npm lifecycle. The hook guarded against the forks-pool orphan swarm, which `pool: 'threads'` already eliminated (threads die with the parent — verified zero lingering processes), so it's removed rather than patched. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016SGQKuUW7a12hBSwmwSkYa
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Replaces the single hard-wired bar chart with a real chart surface, matching the Claude Design handoff.
autoChartheuristic — classifies each column from its ClickHouse type (strippingNullable/LowCardinality): temporal X → line, categorical X → horizontal bar, ordinal X → column. Non-chartable results show a hint instead of a broken axis.K/M+YYYY-MMlabels).Dependency decision
Rendering now uses Chart.js — the one bundled runtime dependency, inlined into
dist/sql.html, so the page still makes zero third-party requests. This relaxes CLAUDE.md hard rule #4 (no runtime deps); the change is recorded inCLAUDE.md,README.md, andbuild/build.mjs. React-only libs (Recharts/visx) were ruled out — the app is framework-free.Keeping the layers honest
All role/axis/pivot/scale math and the Chart.js config builder are pure in
src/core/chart-data.js(100/100/100/100); thenew Chart()call is an injected seam (app.Chart, like the fetch/crypto seams), soresults.jsstays fully tested. Net: coverage never had to drop — only the no-deps rule did. Live Chart instances are destroyed on re-render to avoid canvas/observer leaks.Verification
npm test→ 488 passed, per-file coverage gate green.npm run build→ 313 KB self-containeddist/sql.html(Chart.js inlined).🤖 Generated with Claude Code